#ifndef __GPGPU_H
#define __GPGPU_H

/*This is [0,1] x [0,1] Texture */
#define DATA_INTERNAL GL_RGBA32F
#define DATA_FORMAT GL_RGBA
#define DATA_TYPE GL_FLOAT
#define TYPE_SIZE 4


class TexCoord
{
public:
	TexCoord () : x(0.0f), y (0.0f) {}
	TexCoord (float _x, float _y) : x(_x), y (_y) {}
public:
	float x, y ;
};

class TextureMemory2D
{
public :
	/**
	 * height --> row --> y
	 * width  --> col --> x
	 */
	typedef math::vector<float, TYPE_SIZE> frgba_type ;
	TextureMemory2D (unsigned row, unsigned col) : m_row (row), m_col (col) 
	{
		this->m_videoMemory = new float [TYPE_SIZE * m_row * m_col] ;
		if (this->m_videoMemory == NULL ) ASSERT ("Memory Allocation Fail.");
	}
	unsigned get_col() const {return this->m_col ; }
	unsigned get_row() const {return this->m_row ; }

	float get_texXOffset () const {return 1.0; }
	float get_texYOffset () const {return 1.0 ; }
	float get_texOffset () const { return 1.0;}

	float * get_memory () const { return this->m_videoMemory ;}
	TexCoord get_texCoord (unsigned row, unsigned col ) 
	{
		TexCoord coord ;
		coord.x = col; 
		coord.y = row; 
		return coord ;
	}

	void read (unsigned row, unsigned col, frgba_type & color) 
	{
		unsigned offset = get_offset(row, col ); 
		for (unsigned i = 0 ; i < TYPE_SIZE ; ++i )
		{
			color[i] = m_videoMemory[offset + i]; 
		}
	}
	void write (unsigned row, unsigned col, const frgba_type & color) 
	{
		unsigned offset = get_offset (row, col);
		for (unsigned i = 0 ; i < TYPE_SIZE ; ++i )
		{
			m_videoMemory[offset + i] = color[i]; 
		}	
	}

	void write (unsigned row, unsigned col, unsigned offset, const float value)
	{
		offset += get_offset(row, col);
		m_videoMemory[offset] = value ; 
	}

	~TextureMemory2D () {delete this->m_videoMemory ;}
protected :
	/* Col has high priority. */
	unsigned get_offset (unsigned row, unsigned col) { return TYPE_SIZE * ( row * m_col + col) ; }
private :
	unsigned m_col, m_row ;
	float * m_videoMemory ;

};

class FBO 
{
public :
	FBO () : m_size (0), m_color_att_base(GL_COLOR_ATTACHMENT0), m_max_att_offset(0)  
	{
		glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &m_max_att_offset);
	}
	~ FBO () 
	{
		glDeleteFramebuffersEXT (m_size, &m_framebuffer);
	}
	void create (int size) 
	{
		TEST ( size < m_max_att_offset , "Color Attachment Size is large than its maximum");
		m_size = size ;
		glGenFramebuffers(size, &m_framebuffer);  
		glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffer);
	}
	void bind () {	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_framebuffer);}
	int get_buffer (int i) const { return m_color_att_base + i ;}

	int get_max_size () const {return this->m_max_att_offset ;}
private :
	int m_max_att_offset ;
	int m_color_att_base ;
	int m_size ;
	GLuint m_framebuffer ; 
};

class Texture 
{
public :
	typedef math::vector<float, TYPE_SIZE> frgba_type ;

	Texture (unsigned row, unsigned col)
		: m_row (row),
		m_col (col)
	{
//		mp_texture = new TextureMemory2D (row, col);
		mp_subtexture = new TextureMemory2D (1, 1);
	}
	~ Texture () 
	{
		delete mp_subtexture ;
	}
public :
	// filter could be GL_NEAREST, GL_LINEAR ..
	void set_filter (GLuint filter = GL_NEAREST)
	{
		bind (); 
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
	}
	// wrapper could be GL_MIRRORED_REPEAT , GL_REPEAT, GL_CLAMP ...
	void set_wrapper (GLuint wrapper = GL_MIRRORED_REPEAT )
	{
		bind(); 
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);		
	}
	void set_tex_format ()
	{
		bind(); 
		glTexImage2D (GL_TEXTURE_2D, 0, DATA_INTERNAL, m_row, m_col, 0, DATA_FORMAT, DATA_TYPE, 0);
	}
	void init ()
	{
		create_texture () ;
		set_filter (); 
		set_wrapper (); 
		set_tex_format ();
	}
	GLuint get_handle () const {return m_tex_name ;}

	void set_viewport ()
	{
		glViewport(0, 0, m_row, m_col );
		glMatrixMode(GL_PROJECTION);
		glPushMatrix();
		glLoadIdentity();
		gluOrtho2D(0.0, 1.0, 0.0, 1.0);
		glMatrixMode(GL_MODELVIEW);
		glPushMatrix();
		glLoadIdentity();
		glDisable(GL_DEPTH_TEST);		
	}
	void attach (int color_attachment)
	{
	    // bind tex to color buffer 
		this->m_color_attachment = color_attachment ;
		glFramebufferTexture2D(GL_FRAMEBUFFER,  m_color_attachment, GL_TEXTURE_2D, m_tex_name,0);
		// Draw on color buffer
		
	}
	void draw_buffer () 
	{
		glDrawBuffer (m_color_attachment);
	}
	void write_gpu (unsigned row, unsigned col, const frgba_type & color)
	{
		this->mp_subtexture->write (0, 0, color);
		glTexSubImage2D (GL_TEXTURE_2D, 0, 
						 col, row, 
						 1, 1, DATA_FORMAT, DATA_TYPE, mp_subtexture->get_memory()); 
	}
	void write_gpu(TextureMemory2D & tm) 
	{
		glTexSubImage2D (GL_TEXTURE_2D, 0, 
						 0, 0, 
						 tm.get_row(), tm.get_col(), DATA_FORMAT, DATA_TYPE, tm.get_memory());			
	}
	void read_gpu (unsigned row, unsigned col, frgba_type & color)
	{
		glReadBuffer(m_color_attachment );
		glReadPixels(col, row, 1, 1, DATA_FORMAT, DATA_TYPE, mp_subtexture->get_memory());
		mp_subtexture->read (0, 0, color);
	}
	void read_gpu (TextureMemory2D & result)
	{
		glReadBuffer(m_color_attachment);
		glReadPixels(0, 0, result.get_row(), result.get_col(), DATA_FORMAT, DATA_TYPE, result.get_memory());
	}
	void run (Shader & shader )
	{
		shader.bind(); 
		glPolygonMode(GL_FRONT,GL_FILL);
		glBegin(GL_QUADS);
		glTexCoord2f(0.0, 0.0);  glVertex2f(0.0, 0.0);
		glTexCoord2f(1.0, 0.0);  glVertex2f(1.0, 0.0);
		glTexCoord2f(1.0, 1.0);  glVertex2f(1.0, 1.0);
		glTexCoord2f(0.0, 1.0);  glVertex2f(0.0, 1.0);
		glEnd();		
		shader.unbind(); 
		glFlush(); // Critical. Otherwise, the data cannot reach the painting color buffer attachment. 
	}
	void bind () { glBindTexture (GL_TEXTURE_2D, m_tex_name); }
	void unbind () { glBindTexture (GL_TEXTURE_2D, 0); }
	// auxiliary function 
	void print ()
	{
		TextureMemory2D result (this->m_row, this->m_col);
		frgba_type c ;
		this->read_gpu (result) ;

		for (unsigned i = 0;  i < m_row; ++i)
		{
			for (unsigned j = 0 ; j < m_col; ++j)
			{
				result.read (i,j , c); 
				std::cout<<c<<' ';
			}
			std::cout<<std::endl;
		}
	}
protected :
	void create_texture () {glGenTextures (1, & m_tex_name ); }


private :
//	TextureMemory2D * mp_texture ;
	TextureMemory2D * mp_subtexture ;
	unsigned m_row, m_col ;
	GLuint m_tex_name ;  // Texture Handle 
	GLuint m_framebuffer ; // FBO
	GLint m_color_attachment ;
};

/*
// Examples. 
#include <GL/glew.h>
#include <GL/glut.h>


#include <iostream>
#include <string>
#include <cstring>
#include "config.h"
#include "matrix.h"

#include "ui/shader.h"
#include "ui/glframe.h"
#include "gpgpu.h"

int main (int argc, char * argv[])
{
	glutInit (&argc, argv);
    glutCreateWindow("TEST1");	
	glewInit();

	FBO fbo ;
	fbo.create(3);
	fbo.bind(); 

	Shader shader ;
	shader.load ("../glsl/test.vert", "../glsl/test.frag");


	unsigned row = 4; 
	unsigned col = 2;

	TextureMemory2D data   (row, col);
	typedef Texture::frgba_type frgba_type ;
	for (unsigned i = 0;  i < row; ++i)
		for (unsigned j = 0 ; j < col; ++j)
			data.write (i,j , frgba_type(i,j, i, j)); 

	Texture tex1(row, col) ;  // x = 4 , y = 2; 
	Texture tex2(row, col) ;


	tex1.init ();  
	tex2.init(); 
	tex1.bind(); 
	tex1.write_gpu (data); 
	tex1.write_gpu (0, 1, frgba_type(5, 5, 0, 5));


    // One pass
	//tex1.set_viewport();
	//tex1.attach (fbo.get_buffer (0)) ;
	//tex1.draw_buffer(); 
	//glActiveTexture(GL_TEXTURE0);
	//tex1.run(shader); 

	// Alternate passes
	tex1.set_viewport(); 
	tex1.attach (fbo.get_buffer (0)) ;
	tex2.attach (fbo.get_buffer (1)) ;

	tex2.draw_buffer (); 
	glActiveTexture(GL_TEXTURE0);
	tex1.bind(); 
	tex1.run(shader);

	tex1.draw_buffer () ;
	glActiveTexture(GL_TEXTURE0);
	tex2.bind(); 
	tex2.run(shader);

	std::cout<<"Tex 1"<<std::endl;
	tex1.print();
	std::cout<<"Tex 2"<<std::endl;
	tex2.print();

	return 0;
}
*/

#endif
